Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | /** * API route for skill mastery operations * * POST /api/curriculum/[playerId]/skills - Record a skill attempt * PUT /api/curriculum/[playerId]/skills - Set mastered skills (manual override) * PATCH /api/curriculum/[playerId]/skills - Refresh skill recency (sets lastPracticedAt to now) */ import { NextResponse } from 'next/server' import { withAuth } from '@/lib/auth/withAuth' import { canPerformAction } from '@/lib/classroom' import type { PracticeLevel } from '@/db/schema/player-skill-mastery' import { recordSkillAttempt, refreshSkillRecency, setMasteredSkills, setSkillPracticeLevels, } from '@/lib/curriculum/progress-manager' import { getUserId } from '@/lib/viewer' /** * POST - Record a single skill attempt * Requires 'start-session' permission (parent or teacher-present) */ export const POST = withAuth(async (request, { params }) => { try { const { playerId } = (await params) as { playerId: string } if (!playerId) { return NextResponse.json({ error: 'Player ID required' }, { status: 400 }) } // Authorization: require 'start-session' permission (parent or teacher-present) const userId = await getUserId() const canModify = await canPerformAction(userId, playerId, 'start-session') if (!canModify) { return NextResponse.json({ error: 'Not authorized' }, { status: 403 }) } const body = await request.json() const { skillId, isCorrect } = body if (!skillId) { return NextResponse.json({ error: 'Skill ID required' }, { status: 400 }) } if (typeof isCorrect !== 'boolean') { return NextResponse.json({ error: 'isCorrect must be a boolean' }, { status: 400 }) } const result = await recordSkillAttempt(playerId, skillId, isCorrect) return NextResponse.json(result) } catch (error) { console.error('Error recording skill attempt:', error) return NextResponse.json({ error: 'Failed to record skill attempt' }, { status: 500 }) } }) /** * PUT - Set skill practice levels (teacher manual override) * Requires 'start-session' permission (parent or teacher-present) * Body: { skillLevels: Record<string, PracticeLevel> } * OR: { masteredSkillIds: string[] } (backward compat - treats all as 'visual') */ export const PUT = withAuth(async (request, { params }) => { try { const { playerId } = (await params) as { playerId: string } if (!playerId) { return NextResponse.json({ error: 'Player ID required' }, { status: 400 }) } // Authorization: require 'start-session' permission (parent or teacher-present) const userId = await getUserId() const canModify = await canPerformAction(userId, playerId, 'start-session') if (!canModify) { return NextResponse.json({ error: 'Not authorized' }, { status: 403 }) } const body = await request.json() // New format: { skillLevels: Record<string, PracticeLevel> } if ( body.skillLevels && typeof body.skillLevels === 'object' && !Array.isArray(body.skillLevels) ) { const validLevels: PracticeLevel[] = ['none', 'abacus', 'visual'] const entries = Object.entries(body.skillLevels as Record<string, string>) for (const [key, value] of entries) { if (typeof key !== 'string' || !validLevels.includes(value as PracticeLevel)) { return NextResponse.json( { error: `Invalid skill level for "${key}": "${value}". Must be one of: ${validLevels.join(', ')}`, }, { status: 400 } ) } } const result = await setSkillPracticeLevels( playerId, body.skillLevels as Record<string, PracticeLevel> ) return NextResponse.json(result) } // Legacy format: { masteredSkillIds: string[] } const { masteredSkillIds } = body if (!Array.isArray(masteredSkillIds)) { return NextResponse.json( { error: 'Body must contain skillLevels (object) or masteredSkillIds (array)' }, { status: 400 } ) } // Validate that all items are strings if (!masteredSkillIds.every((id: unknown) => typeof id === 'string')) { return NextResponse.json({ error: 'All skill IDs must be strings' }, { status: 400 }) } const result = await setMasteredSkills(playerId, masteredSkillIds) return NextResponse.json(result) } catch (error) { console.error('Error setting skill levels:', error) return NextResponse.json({ error: 'Failed to set skill levels' }, { status: 500 }) } }) /** * PATCH - Refresh skill recency by inserting a sentinel record * Requires 'start-session' permission (parent or teacher-present) * Body: { skillId: string } * * Use this when a teacher wants to mark a skill as "recently practiced" * (e.g., student did offline workbooks). * * This inserts a "recency-refresh" sentinel record that: * - Updates lastPracticedAt in BKT (resets staleness) * - Does NOT affect pKnown (zero-weight for mastery calculation) * * Returns: { sessionId: string, timestamp: Date } or 404 if skill not found */ export const PATCH = withAuth(async (request, { params }) => { try { const { playerId } = (await params) as { playerId: string } if (!playerId) { return NextResponse.json({ error: 'Player ID required' }, { status: 400 }) } // Authorization: require 'start-session' permission (parent or teacher-present) const userId = await getUserId() const canModify = await canPerformAction(userId, playerId, 'start-session') if (!canModify) { return NextResponse.json({ error: 'Not authorized' }, { status: 403 }) } const body = await request.json() const { skillId } = body if (!skillId || typeof skillId !== 'string') { return NextResponse.json({ error: 'Skill ID required (string)' }, { status: 400 }) } const result = await refreshSkillRecency(playerId, skillId) if (!result) { return NextResponse.json({ error: 'Skill not found for this player' }, { status: 404 }) } return NextResponse.json(result) } catch (error) { console.error('Error refreshing skill recency:', error) return NextResponse.json({ error: 'Failed to refresh skill recency' }, { status: 500 }) } }) |